Recommendation system

Problem formulation

The goal of our project is to develop a recommendation system trained on beauty products data from Amazon. Based on some studies it has been proven that personalized product recommendations drive 24% of the orders and 26% of the revenue. This explains the influence recommendation has on volume of orders and generally on sales figures. What is more, it has been proven that product recommendations lead to reoccurring visits and that purchases on recommendation mark higher average-order value. Consequently, we decided to use method called user-based collaborative filtering to build our recommendation system (Reference).

First, we proceed with data preparation and pre-processing, then we build our recommender system, and finally draw business implications.

Data collection

As we earlier mentioned, we use data on Amazon customer reviews of beauty products. The data used in this project can be accessed in this link. It contains the following features:

  • Product price: price of the product
  • Product Id: ASIN number of a product on Amazon.
  • Product title: title of the product
  • Review helpfulness: fraction of users who found the review helpful
  • Profile Name: name of the user
  • Review score: rating of the product
  • Review summary: concise summary of the review text
  • Review text
  • Review time
  • Review userId

Data preparation and preprocessing

Packages

#Packages
library(R.utils)
library(dplyr)
library(tidyr)
library(janitor)
library(recommenderlab)
library(tm)
library(NLP)
library(qdap)
library(readr)
library(wordcloud)

Data loading and inspecting

After downloading data locally we load in data by usingreadLines() function:

# Loading in data
my_data <- readLines(gzfile("data/Beauty.txt.gz"))

Let us have a first at the dimension of our data:

length(my_data)
[1] 2772616
Warning in for (name in names(hooks)) { :
  closing unused connection 3 (data/Beauty.txt.gz)

Our data set is currently in a form of a vector with 10 elements. Obviously, this is not the optimal form of the data we would like to work with. That is why we need to work around this data set to make it more convenient for further analysis.

head(my_data)
[1] "product/productId: B00064C0IU"                                      
[2] "product/title: Oscar Eau de Toilette for Women by Oscar de La Renta"
[3] "product/price: 24.19"                                               
[4] "review/userId: A1FWT811DSZLC8"                                      
[5] "review/profileName: Heidi M"                                        
[6] "review/helpfulness: 0/0"                                            

From the structure of our data set, we see that all values are stored in the first column. Thus, we would need to manipulate data in order to be able to process it further.

What we can already do is to remove all fields with no characters:

my_data <- my_data[sapply(my_data, nchar) > 0]

Then we can convert it to data frame:

my_data <- as.data.frame(my_data)
colnames(my_data) <- "product"

One of the critical steps is separating the column to multiple columns:

# Separate one column to two (":" separator)
my_data <- separate(my_data,col = product, into = c("Info","Product"), sep = ":")

Inspecting first 10 values:

head(my_data,10)

The data set is loaded in .txt format, and all values were stored in two columns, which makes it a bit challenging to work with. In the following sections we will undertake data manipulation in order to bring the data set in more suitable form.

First, we will convert it from the current long-format (19828, 10) to the wide-format, where each column will represent a product, and each row a feature:

#Converting long format to wide
my_data <- my_data %>%
  group_by(Info) %>%
  mutate(Order = seq_along(Info)) %>%
  spread(key = Order, value = Product)

Since the column names are labeled with numbers, we will apply first row as a label for the corresponding column name:

my_data <- as.data.frame(t(my_data))
my_data<-my_data%>%
  row_to_names(row_number = 1)

Delete rows with at least 1 NAs:

my_data <- my_data[rowSums(is.na(my_data))==0,]

Trim white space at the beginning or ending the string:

my_data$`review/userId`<- trimws(my_data$`review/userId`)
my_data$`product/productId`<- trimws(my_data$`product/productId`)
my_data$`product/price`<- trimws(my_data$`product/price`)
my_data$`product/title`<- trimws(my_data$`product/title`)

Filtering out reviews with unknown userID and productId:

my_data<-filter(my_data,`review/userId`!="unknown" & `product/productId`!="unknown" & `product/price`!="unknown")

Correcting column classes:

my_data$`product/productId` <- as.factor(my_data$`product/productId`)
my_data$`review/score`<- as.numeric(my_data$`review/score`)
my_data$`review/userId`<-as.factor(my_data$`review/userId`)
my_data$`product/price`<-as.numeric(my_data$`product/price`)

How many times users reviewed products?

In order to use relevant data, we would need to define the minimum number of reviews per user. Let us inspect the distribution of number of reviews:

(freq<-as.data.frame(table(my_data$`review/userId`)))
table(freq$Freq)

     1      2      3      4      5      6      7      8      9     10     11     12 
115652  17685   5563   2512    945    528    383    186    238    111     66     65 
    13     14     15     16     17     18     19     20     21     22     23     24 
    30     35    260    135     45     31     29     24     26     13     15     82 
    25     26     27     28     29     30     31     32     34     35     36     37 
    18      9      5      6      3     10      3      3      2      6      7      4 
    38     39     41     43     45     46     47     48     49     50     53     59 
     5      4      2      2      4      2      1      1      1      1      1      2 
    62     64     75     85     86    158    205    561 
     1      1      1      2      1      1      1      1 

It seems that majority of users left only one review. Therefore, we will remove all single-review users.

Filtering out users who left 6 or more reviews:

index<-filter(freq, freq$Freq>=9)$Var1

We are now left with 5 users who reviewed certain beauty product at least 6 times.

(my_data <- subset(my_data,`review/userId` %in% index))

Exploratory data analysis

How many unique products are reviewed?

length(unique(my_data$`product/productId`))
[1] 4045

There are 4045 products which were reviewed.

How many reviewers do we have?

length(unique(my_data$`review/userId`))
[1] 1078

There are 1078 unique reviewers/customers who reviewed products.

How many scores do we have?

length(my_data$`review/score`)
[1] 211791

There are 19828 ratings.

What is the distribution of ratings?

hist(as.numeric(my_data$`review/score`),main = "Histogramm of scores",xlab = "Score")

Products seem to be favorably rated as the distribution of scores showes that the best score is the most frequent.

What is the average number of reviews per user?

my_data %>% 
  group_by(`review/userId`) %>%
  summarise(Freq=n()) %>% 
  summary()
               review/userId       Freq       
 A10412572BPZJM6QSB69S:   1   Min.   : 10.00  
 A104D32SF6TX7F       :   1   1st Qu.: 13.00  
 A10IQD569MWNGU       :   1   Median : 15.00  
 A1115ST6F5CWYP       :   1   Mean   : 18.39  
 A111Z6YLF7VARM       :   1   3rd Qu.: 20.00  
 A112JF58KKB8LP       :   1   Max.   :561.00  
 (Other)              :1072                   

In the original data set It users left on average left a review only once. After filtering, we see that our average is at 3 reviews per user.

What is the average score per user?

(grand.mean <-my_data %>% 
  group_by(`review/userId`) %>%
  dplyr::summarise(Mean=mean(`review/score`)) %>%
  mutate(Grand.mean=mean(Mean))%>%
  head())

It seems that beauty products on Amazon are well received by users as the average score per user is quite high, at 4.2023202.

Building a model

Final data outlook

Here is a glimpse in our data before we start building the recommnder:

(my_data)

Subsetting data

In order to model a recommender system, three variables in our case are of great importance:

  • User ID
  • Product ID
  • Score / Rating

Our model will be based on these three variables. Additionally, we will make use of the remaining features by utilizing some text mining techniques, but you will find more details at some later point. Now, we will make a subset of our data with 3 mentioned variables:

subset_my_data <- subset(my_data, select = c(`review/userId`,`product/productId`,`review/score`))
head(subset_my_data)
NA

Let us inspect the dimensions:

dim(subset_my_data)
[1] 19828     3

Formatting data

Our data is currently in the long format, i.e. one row for one rating. However, we would want to get a matrix with ratings where the rows represent the users IDs and the columns the Product IDs. Thus, we will transform our data to so called rating matrix:

ratings <- as(subset_my_data, "realRatingMatrix")

In order to avoid “high/low rating bias” from users who give high (or low) ratings to all the products they reviewed, we will need to normalize our data. That would prevent certain bias in the results.

ratings <- normalize(ratings)

Inspecting real rating matrix

We can plot an image of the rating matrix for the first 250 users and 250 products:

image(ratings[1:250,1:250])

From the visualisation we can see that rating matrix is very sparse, i.e. that not every user did rate/review every product in our data set.

We can inspect the data for the first 10 users and the first 4 products:

ratings[1:10, 1:4]@data
10 x 4 sparse Matrix of class "dgCMatrix"
                      B00004RF1H B00004U9UY B000050B6X B000050B6Y
A10412572BPZJM6QSB69S          .          .          .          .
A104D32SF6TX7F                 .          .          .          .
A10IQD569MWNGU                 .          .          .          .
A1115ST6F5CWYP                 .          .          .          .
A111Z6YLF7VARM                 .          .          .          .
A112JF58KKB8LP                 .          .          .          .
A1159DQXCJXDNN                 .          .          .          .
A117GF5NSKVZ55                 .          .          .          .
A11B8JNLONAAPU                 .          .          .          .
A11JBMQKFTX24O                 .          .          .          .

As we already saw in the visualisation, the data is sparse and the first 10 users did not review first 4 products visualised in the matrix above.

Building a recommender

Finally, we will now build our recommendation system based on User-based collaborative filtering User-based collaborative filtering search for similar users and gives them recommendations based on what other users with similar rating patterns appreciated:

recommender <- Recommender(ratings, method="UBCF")
recommender
Recommender of type ‘UBCF’ for ‘realRatingMatrix’ 
learned using 1078 users.

Additionally, in order to compare results of two methods, we would like to apply item-based collaborative filtering method to build another recommender system. In contrast to user-based collaborative filtering, item-based collaborative filtering looks for similarity patterns between items and recommends them to users based on the computed information.

recommenderIBCF
Recommender of type ‘IBCF’ for ‘realRatingMatrix’ 
learned using 1078 users.

As reported, both recommendation systems are built using 8002 users.

Interpretation and managerial implications

Now we would like to interpret the output of our recommender systems. First we start with UBCF-based recommender system.

current.user <- 45
recommendations <- predict(recommender, current.user, data = ratings, n = 5)

We decided to take user number 45 and inspect 5 recommendations provided to him/her. Now we can inspect what our recommendation system provided in the end:

str(recommendations)
Formal class 'topNList' [package "recommenderlab"] with 4 slots
  ..@ items     :List of 1
  .. ..$ A15F0BRWXSG1YL: int [1:5] 3275 2055 339 348 610
  ..@ ratings   :List of 1
  .. ..$ A15F0BRWXSG1YL: num [1:5] 14.54 14.1 13.54 9.54 8.7
  ..@ itemLabels: chr [1:4045] "B00004RF1H" "B00004U9UY" "B000050B6X" "B000050B6Y" ...
  ..@ n         : int 5

We can see that the user ID of the user number 45 is A10N19OL0CKYDV. Our system found 2 products to recommend to this user, and we can find product index (173, 772) as well as ratings that the system calculated from the ratings of the closest users (5,5).

Let us create a prediction made by IBCF-based recommender:

recommendationsIBCF <- predict(recommenderIBCF,current.user,data = ratings, n=5)
x was already normalized by row!
str(recommendationsIBCF)
Formal class 'topNList' [package "recommenderlab"] with 4 slots
  ..@ items     :List of 1
  .. ..$ A15F0BRWXSG1YL: int [1:5] 3811 2189 3329 1443 967
  ..@ ratings   :List of 1
  .. ..$ A15F0BRWXSG1YL: num [1:5] 13.68 11.85 10.85 9.85 8.85
  ..@ itemLabels: chr [1:4045] "B00004RF1H" "B00004U9UY" "B000050B6X" "B000050B6Y" ...
  ..@ n         : int 5

We will inspect potential recommended products:

head(as(recommendationsIBCF,"list"))
$A15F0BRWXSG1YL
[1] "B000OYYKVC" "B000BRO5DK" "B000KIKE94" "B0006J9TKC" "B00028OC4K"

Unfortunately, our item-based collaborative filtering system did not generate any recommendation for the user number 45.

Implications

As we could see, this user reviewed only one product, called “Opi Ridge Filler .5 oz.”, and it is a nail-care product. We could assume that this person is a female user since the product she bought is typically associated with female beauty care. What is more, two recommended products are as well very strongly associated to being typical female beauty products. Finally, we have the name of the user (Erica), so we can be sure that the user is a female. From the qualitative perspective it seems that our recommendation system provides descent recommendations!.

Bonus analysis: Text Mining

In addition to our recommender system, we will apply some basic text mining techniques to explore reviews text. Text mining helps us to mine opinions of users (in this case) about the reviewed products at scale.

Wordcloud

Here we create a wordcloud of words from product reviews of recommended products to the user 45. Beforehand we would need to pre-process the text of reviews in the following manner:

# Split text into parts using new line character:
text.docs <- Corpus(VectorSource(recommendation_26$`review/text`))
toSpace <- content_transformer(function (x , pattern ) gsub(pattern, " ", x))
text.docs <- tm_map(text.docs, toSpace, "/")
text.docs <- tm_map(text.docs, toSpace, "@")
text.docs <- tm_map(text.docs, toSpace, "\\|")
text.docs <- tm_map(text.docs, content_transformer(tolower))
text.docs <- tm_map(text.docs, removeNumbers)
text.docs <- tm_map(text.docs, stripWhitespace)
text.docs <- tm_map(text.docs, removeWords, stopwords("english"))
text.docs <- tm_map(text.docs, removePunctuation)
dtm <- DocumentTermMatrix(text.docs, control=list(weighting=weightTf))
m <- as.matrix(t(dtm))
v <- sort(rowSums(m),decreasing=TRUE)
d <- data.frame(word = names(v),freq=v)
set.seed(1234)
wordcloud(words = d$word, freq = d$freq, min.freq = 10,
          max.words=200, random.order=FALSE, rot.per=0.35,
          colors=brewer.pal(8, "Dark2"))

From the wordcloud we can see that words “color”, “hair” and “gloves” are quite frequent in the text corpus analyzed. That could be a hint that the user was referring to the usage of the product. The term “cheap” could be easily spotted as well. This word is not very likable among marketers as it brings unfavorable image to the brand. Nevertheless, it seems that the user believes that the product is affordable.

Future work

This data set provides multiple possibility for the further analysis besides recommender systems. Here are some ideas what can be further done:

  • Sentiment analysis - Sentiment analysis can be done and scores (typically from -3 to +3) accompanied to each review description. That would tell us more about the sentiment that users have about the products reviewed.

  • Prediction of ratings - In case that we would have enough data (ratings) about one product, regardless of customers, it would be possible to develop a machine learning model which based on current features (e.g. price) and additional features (such as sentiment or words in the review) could predict the rating that one product might have.

  • Prediction of the sentiment - in the similar manner as the previous point, it would be useful to train a machine learning model to predict a sentiment that would hypotetically emerge in a reviewer.

  • Topic modeling - topic modeling is unsupervised machine learning technique that could help us identify topics which users discuss in the text of reviews.

Limitations

Limitation related to this data set and building a recommender system is the fact that the majority of users have left only one review:

table(as.data.frame(table(my_data$`review/userId`))$Freq)

     1      2      3      4      5      6      7      8      9     10 
115652  17685   5563   2512    945    528    383    186    238    111 
    11     12     13     14     15     16     17     18     19     20 
    66     65     30     35    260    135     45     31     29     24 
    21     22     23     24     25     26     27     28     29     30 
    26     13     15     82     18      9      5      6      3     10 
    31     32     34     35     36     37     38     39     41     43 
     3      3      2      6      7      4      5      4      2      2 
    45     46     47     48     49     50     53     59     62     64 
     4      2      1      1      1      1      1      2      1      1 
    75     85     86    158    205    561 
     1      2      1      1      1      1 

Let us take a look which users left the most reviews:

limitations <-as.data.frame(table(my_data$`review/userId`))
limitations %>% arrange(desc(Freq))%>%rename(UserID=Var1)

We can see that users under IDs A3M174IC0VXOS2,A3KEZLJ59C1JVH,A3QEE0ZPMT3W6P are rare examples of users who left multiple product reviews.

LS0tDQp0aXRsZTogIjA3LVJlY29tbWVuZGF0aW9uX1N5c3RlbSINCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDoNCiAgICB0b2M6IHllcw0KICAgIGRmX3ByaW50OiBwYWdlZA0KICBodG1sX25vdGVib29rOiBkZWZhdWx0DQogIHBkZl9kb2N1bWVudDoNCiAgICB0b2M6IHllcw0KLS0tDQoNCiMgUmVjb21tZW5kYXRpb24gc3lzdGVtDQoNCiMjIFByb2JsZW0gZm9ybXVsYXRpb24NCg0KVGhlIGdvYWwgb2Ygb3VyIHByb2plY3QgaXMgdG8gZGV2ZWxvcCBhIHJlY29tbWVuZGF0aW9uIHN5c3RlbSB0cmFpbmVkIG9uIGJlYXV0eSBwcm9kdWN0cyBkYXRhIGZyb20gQW1hem9uLg0KQmFzZWQgb24gc29tZSBzdHVkaWVzIGl0IGhhcyBiZWVuIHByb3ZlbiB0aGF0IHBlcnNvbmFsaXplZCBwcm9kdWN0IHJlY29tbWVuZGF0aW9ucyBkcml2ZSAyNCUgb2YgdGhlIG9yZGVycyBhbmQgMjYlIG9mIHRoZSByZXZlbnVlLiBUaGlzIGV4cGxhaW5zIHRoZSBpbmZsdWVuY2UgcmVjb21tZW5kYXRpb24gaGFzIG9uIHZvbHVtZSBvZiBvcmRlcnMgYW5kIGdlbmVyYWxseSBvbiBzYWxlcyBmaWd1cmVzLiBXaGF0IGlzIG1vcmUsIGl0IGhhcyBiZWVuIHByb3ZlbiB0aGF0IHByb2R1Y3QgcmVjb21tZW5kYXRpb25zIGxlYWQgdG8gcmVvY2N1cnJpbmcgdmlzaXRzIGFuZCB0aGF0IHB1cmNoYXNlcyBvbiByZWNvbW1lbmRhdGlvbiBtYXJrIGhpZ2hlciBhdmVyYWdlLW9yZGVyIHZhbHVlLiBDb25zZXF1ZW50bHksIHdlIGRlY2lkZWQgdG8gdXNlIG1ldGhvZCBjYWxsZWQgdXNlci1iYXNlZCBjb2xsYWJvcmF0aXZlIGZpbHRlcmluZyB0byBidWlsZCBvdXIgcmVjb21tZW5kYXRpb24gc3lzdGVtICgqW1JlZmVyZW5jZV0oaHR0cHM6Ly93d3cuc2FsZXNmb3JjZS5jb20vYmxvZy8yMDE3LzExL3BlcnNvbmFsaXplZC1wcm9kdWN0LXJlY29tbWVuZGF0aW9ucy1kcml2ZS1qdXN0LTctdmlzaXRzLTI2LXJldmVudWUpKikuDQoNCkZpcnN0LCB3ZSBwcm9jZWVkIHdpdGggZGF0YSBwcmVwYXJhdGlvbiBhbmQgcHJlLXByb2Nlc3NpbmcsIHRoZW4gd2UgYnVpbGQgb3VyIHJlY29tbWVuZGVyIHN5c3RlbSwgYW5kIGZpbmFsbHkgZHJhdyBidXNpbmVzcyBpbXBsaWNhdGlvbnMuDQoNCiMjIERhdGEgY29sbGVjdGlvbg0KDQpBcyB3ZSBlYXJsaWVyIG1lbnRpb25lZCwgd2UgdXNlIGRhdGEgb24gQW1hem9uIGN1c3RvbWVyIHJldmlld3Mgb2YgYmVhdXR5IHByb2R1Y3RzLiBUaGUgZGF0YSB1c2VkIGluIHRoaXMgcHJvamVjdCBjYW4gYmUgYWNjZXNzZWQgaW4gdGhpcyBbbGlua10oaHR0cDovL3NuYXAuc3RhbmZvcmQuZWR1L2RhdGEvd2ViLUFtYXpvbi1saW5rcy5odG1sKS4gSXQgY29udGFpbnMgdGhlIGZvbGxvd2luZyBmZWF0dXJlczoNCg0KKiBQcm9kdWN0IHByaWNlOiBwcmljZSBvZiB0aGUgcHJvZHVjdA0KKiBQcm9kdWN0IElkOiBBU0lOIG51bWJlciBvZiBhIHByb2R1Y3Qgb24gQW1hem9uLg0KKiBQcm9kdWN0IHRpdGxlOiB0aXRsZSBvZiB0aGUgcHJvZHVjdA0KKiBSZXZpZXcgaGVscGZ1bG5lc3M6IGZyYWN0aW9uIG9mIHVzZXJzIHdobyBmb3VuZCB0aGUgcmV2aWV3IGhlbHBmdWwNCiogUHJvZmlsZSBOYW1lOiBuYW1lIG9mIHRoZSB1c2VyDQoqIFJldmlldyBzY29yZTogcmF0aW5nIG9mIHRoZSBwcm9kdWN0DQoqIFJldmlldyBzdW1tYXJ5OiBjb25jaXNlIHN1bW1hcnkgb2YgdGhlIHJldmlldyB0ZXh0DQoqIFJldmlldyB0ZXh0CQ0KKiBSZXZpZXcgdGltZQ0KKiBSZXZpZXcgdXNlcklkDQoNCiMjIERhdGEgcHJlcGFyYXRpb24gYW5kIHByZXByb2Nlc3NpbmcNCg0KIyMjIFBhY2thZ2VzDQoNCmBgYHtyLHdhcm5pbmc9RkFMU0UsZXJyb3I9RkFMU0UsbWVzc2FnZT1GQUxTRX0NCiNQYWNrYWdlcw0KbGlicmFyeShSLnV0aWxzKQ0KbGlicmFyeShkcGx5cikNCmxpYnJhcnkodGlkeXIpDQpsaWJyYXJ5KGphbml0b3IpDQpsaWJyYXJ5KHJlY29tbWVuZGVybGFiKQ0KbGlicmFyeSh0bSkNCmxpYnJhcnkoTkxQKQ0KbGlicmFyeShxZGFwKQ0KbGlicmFyeShyZWFkcikNCmxpYnJhcnkod29yZGNsb3VkKQ0KYGBgDQoNCiMjIyBEYXRhIGxvYWRpbmcgYW5kIGluc3BlY3RpbmcNCg0KQWZ0ZXIgZG93bmxvYWRpbmcgZGF0YSBsb2NhbGx5IHdlIGxvYWQgaW4gZGF0YSBieSB1c2luZ2ByZWFkTGluZXMoKWAgZnVuY3Rpb246DQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQ0KIyBMb2FkaW5nIGluIGRhdGENCm15X2RhdGEgPC0gcmVhZExpbmVzKGd6ZmlsZSgiZGF0YS9CZWF1dHkudHh0Lmd6IikpDQpgYGANCg0KTGV0IHVzIGhhdmUgYSBmaXJzdCBhdCB0aGUgZGltZW5zaW9uIG9mIG91ciBkYXRhOg0KDQpgYGB7cn0NCmxlbmd0aChteV9kYXRhKQ0KYGBgDQoNCk91ciBkYXRhIHNldCBpcyBjdXJyZW50bHkgaW4gYSBmb3JtIG9mIGEgdmVjdG9yIHdpdGggYHIgbGVuZ3RoKG15X2RhdGEpYCBlbGVtZW50cy4gT2J2aW91c2x5LCB0aGlzIGlzIG5vdCB0aGUgb3B0aW1hbCBmb3JtIG9mIHRoZSBkYXRhIHdlIHdvdWxkIGxpa2UgdG8gd29yayB3aXRoLiBUaGF0IGlzIHdoeSB3ZSBuZWVkIHRvIHdvcmsgYXJvdW5kIHRoaXMgZGF0YSBzZXQgdG8gbWFrZSBpdCBtb3JlIGNvbnZlbmllbnQgZm9yIGZ1cnRoZXIgYW5hbHlzaXMuIA0KDQpgYGB7cn0NCmhlYWQobXlfZGF0YSkNCmBgYA0KDQpGcm9tIHRoZSBzdHJ1Y3R1cmUgb2Ygb3VyIGRhdGEgc2V0LCB3ZSBzZWUgdGhhdCBhbGwgdmFsdWVzIGFyZSBzdG9yZWQgaW4gdGhlIGZpcnN0IGNvbHVtbi4gVGh1cywgd2Ugd291bGQgbmVlZCB0byBtYW5pcHVsYXRlIGRhdGEgaW4gb3JkZXIgdG8gYmUgYWJsZSB0byBwcm9jZXNzIGl0IGZ1cnRoZXIuDQoNCldoYXQgd2UgY2FuIGFscmVhZHkgZG8gaXMgdG8gcmVtb3ZlIGFsbCBmaWVsZHMgd2l0aCBubyBjaGFyYWN0ZXJzOg0KDQpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRX0NCm15X2RhdGEgPC0gbXlfZGF0YVtzYXBwbHkobXlfZGF0YSwgbmNoYXIpID4gMF0NCmBgYA0KDQpUaGVuIHdlIGNhbiBjb252ZXJ0IGl0IHRvIGRhdGEgZnJhbWU6DQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQ0KbXlfZGF0YSA8LSBhcy5kYXRhLmZyYW1lKG15X2RhdGEpDQpjb2xuYW1lcyhteV9kYXRhKSA8LSAicHJvZHVjdCINCmBgYA0KDQpPbmUgb2YgdGhlIGNyaXRpY2FsIHN0ZXBzIGlzIHNlcGFyYXRpbmcgdGhlIGNvbHVtbiB0byBtdWx0aXBsZSBjb2x1bW5zOg0KDQpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRX0NCiMgU2VwYXJhdGUgb25lIGNvbHVtbiB0byB0d28gKCI6IiBzZXBhcmF0b3IpDQpteV9kYXRhIDwtIHNlcGFyYXRlKG15X2RhdGEsY29sID0gcHJvZHVjdCwgaW50byA9IGMoIkluZm8iLCJQcm9kdWN0IiksIHNlcCA9ICI6IikNCmBgYA0KDQpJbnNwZWN0aW5nIGZpcnN0IDEwIHZhbHVlczoNCg0KYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0V9DQpoZWFkKG15X2RhdGEsMTApDQpgYGANCg0KVGhlIGRhdGEgc2V0IGlzIGxvYWRlZCBpbiAudHh0IGZvcm1hdCwgYW5kIGFsbCB2YWx1ZXMgd2VyZSBzdG9yZWQgaW4gdHdvIGNvbHVtbnMsIHdoaWNoIG1ha2VzIGl0IGEgYml0IGNoYWxsZW5naW5nIHRvIHdvcmsgd2l0aC4gSW4gdGhlIGZvbGxvd2luZyBzZWN0aW9ucyB3ZSB3aWxsIHVuZGVydGFrZSBkYXRhIG1hbmlwdWxhdGlvbiBpbiBvcmRlciB0byBicmluZyB0aGUgZGF0YSBzZXQgaW4gbW9yZSBzdWl0YWJsZSBmb3JtLiANCg0KRmlyc3QsIHdlIHdpbGwgY29udmVydCBpdCBmcm9tIHRoZSBjdXJyZW50IGxvbmctZm9ybWF0IChgciBkaW0obXlfZGF0YSlgKSB0byB0aGUgd2lkZS1mb3JtYXQsIHdoZXJlIGVhY2ggY29sdW1uIHdpbGwgcmVwcmVzZW50IGEgcHJvZHVjdCwgYW5kIGVhY2ggcm93IGEgZmVhdHVyZToNCg0KYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0V9DQojQ29udmVydGluZyBsb25nIGZvcm1hdCB0byB3aWRlDQpteV9kYXRhIDwtIG15X2RhdGEgJT4lDQogIGdyb3VwX2J5KEluZm8pICU+JQ0KICBtdXRhdGUoT3JkZXIgPSBzZXFfYWxvbmcoSW5mbykpICU+JQ0KICBzcHJlYWQoa2V5ID0gT3JkZXIsIHZhbHVlID0gUHJvZHVjdCkNCmBgYA0KDQpTaW5jZSB0aGUgY29sdW1uIG5hbWVzIGFyZSBsYWJlbGVkIHdpdGggbnVtYmVycywgd2Ugd2lsbCBhcHBseSBmaXJzdCByb3cgYXMgYSBsYWJlbCBmb3IgdGhlIGNvcnJlc3BvbmRpbmcgY29sdW1uIG5hbWU6DQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQ0KbXlfZGF0YSA8LSBhcy5kYXRhLmZyYW1lKHQobXlfZGF0YSkpDQpteV9kYXRhPC1teV9kYXRhJT4lDQogIHJvd190b19uYW1lcyhyb3dfbnVtYmVyID0gMSkNCmBgYA0KDQoNCkRlbGV0ZSByb3dzIHdpdGggYXQgbGVhc3QgMSBOQXM6DQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQ0KbXlfZGF0YSA8LSBteV9kYXRhW3Jvd1N1bXMoaXMubmEobXlfZGF0YSkpPT0wLF0NCmBgYA0KDQpUcmltIHdoaXRlIHNwYWNlIGF0IHRoZSBiZWdpbm5pbmcgb3IgZW5kaW5nIHRoZSBzdHJpbmc6DQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQ0KbXlfZGF0YSRgcmV2aWV3L3VzZXJJZGA8LSB0cmltd3MobXlfZGF0YSRgcmV2aWV3L3VzZXJJZGApDQpteV9kYXRhJGBwcm9kdWN0L3Byb2R1Y3RJZGA8LSB0cmltd3MobXlfZGF0YSRgcHJvZHVjdC9wcm9kdWN0SWRgKQ0KbXlfZGF0YSRgcHJvZHVjdC9wcmljZWA8LSB0cmltd3MobXlfZGF0YSRgcHJvZHVjdC9wcmljZWApDQpteV9kYXRhJGBwcm9kdWN0L3RpdGxlYDwtIHRyaW13cyhteV9kYXRhJGBwcm9kdWN0L3RpdGxlYCkNCmBgYA0KDQoNCkZpbHRlcmluZyBvdXQgcmV2aWV3cyB3aXRoIHVua25vd24gdXNlcklEIGFuZCBwcm9kdWN0SWQ6DQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQ0KbXlfZGF0YTwtZmlsdGVyKG15X2RhdGEsYHJldmlldy91c2VySWRgIT0idW5rbm93biIgJiBgcHJvZHVjdC9wcm9kdWN0SWRgIT0idW5rbm93biIgJiBgcHJvZHVjdC9wcmljZWAhPSJ1bmtub3duIikNCmBgYA0KDQpDb3JyZWN0aW5nIGNvbHVtbiBjbGFzc2VzOg0KDQpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRX0NCm15X2RhdGEkYHByb2R1Y3QvcHJvZHVjdElkYCA8LSBhcy5mYWN0b3IobXlfZGF0YSRgcHJvZHVjdC9wcm9kdWN0SWRgKQ0KYGBgDQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQ0KbXlfZGF0YSRgcmV2aWV3L3Njb3JlYDwtIGFzLm51bWVyaWMobXlfZGF0YSRgcmV2aWV3L3Njb3JlYCkNCm15X2RhdGEkYHJldmlldy91c2VySWRgPC1hcy5mYWN0b3IobXlfZGF0YSRgcmV2aWV3L3VzZXJJZGApDQpteV9kYXRhJGBwcm9kdWN0L3ByaWNlYDwtYXMubnVtZXJpYyhteV9kYXRhJGBwcm9kdWN0L3ByaWNlYCkNCmBgYA0KDQoNCiMjIyBIb3cgbWFueSB0aW1lcyB1c2VycyByZXZpZXdlZCBwcm9kdWN0cz8NCg0KSW4gb3JkZXIgdG8gdXNlIHJlbGV2YW50IGRhdGEsIHdlIHdvdWxkIG5lZWQgdG8gZGVmaW5lIHRoZSBtaW5pbXVtIG51bWJlciBvZiByZXZpZXdzIHBlciB1c2VyLiBMZXQgdXMgaW5zcGVjdCB0aGUgZGlzdHJpYnV0aW9uIG9mIG51bWJlciBvZiByZXZpZXdzOg0KDQpgYGB7cn0NCihmcmVxPC1hcy5kYXRhLmZyYW1lKHRhYmxlKG15X2RhdGEkYHJldmlldy91c2VySWRgKSkpDQp0YWJsZShmcmVxJEZyZXEpDQpgYGANCkl0IHNlZW1zIHRoYXQgbWFqb3JpdHkgb2YgdXNlcnMgbGVmdCBvbmx5IG9uZSByZXZpZXcuIFRoZXJlZm9yZSwgd2Ugd2lsbCByZW1vdmUgYWxsIHNpbmdsZS1yZXZpZXcgdXNlcnMuIA0KDQoNCkZpbHRlcmluZyBvdXQgdXNlcnMgd2hvIGxlZnQgNiBvciBtb3JlIHJldmlld3M6DQpgYGB7cn0NCmluZGV4PC1maWx0ZXIoZnJlcSwgZnJlcSRGcmVxPj05KSRWYXIxDQpgYGANCg0KV2UgYXJlIG5vdyBsZWZ0IHdpdGggYHIgbGVuZ3RoKGluZGV4KWAgdXNlcnMgd2hvIHJldmlld2VkIGNlcnRhaW4gYmVhdXR5IHByb2R1Y3QgYXQgbGVhc3QgNiB0aW1lcy4NCg0KYGBge3J9DQoobXlfZGF0YSA8LSBzdWJzZXQobXlfZGF0YSxgcmV2aWV3L3VzZXJJZGAgJWluJSBpbmRleCkpDQpgYGANCg0KIyMjIEV4cGxvcmF0b3J5IGRhdGEgYW5hbHlzaXMNCg0KIyMjIyBIb3cgbWFueSB1bmlxdWUgcHJvZHVjdHMgYXJlIHJldmlld2VkPw0KDQpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRX0NCmxlbmd0aCh1bmlxdWUobXlfZGF0YSRgcHJvZHVjdC9wcm9kdWN0SWRgKSkNCmBgYA0KDQpUaGVyZSBhcmUgYHIgbGVuZ3RoKHVuaXF1ZShteV9kYXRhJCdwcm9kdWN0L3Byb2R1Y3RJZCcpKWAgcHJvZHVjdHMgd2hpY2ggd2VyZSByZXZpZXdlZC4NCg0KDQojIyMjIEhvdyBtYW55IHJldmlld2VycyBkbyB3ZSBoYXZlPw0KDQpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRX0NCmxlbmd0aCh1bmlxdWUobXlfZGF0YSRgcmV2aWV3L3VzZXJJZGApKQ0KYGBgDQoNClRoZXJlIGFyZSBgciBsZW5ndGgodW5pcXVlKG15X2RhdGEkJ3Jldmlldy91c2VySWQnKSlgIHVuaXF1ZSByZXZpZXdlcnMvY3VzdG9tZXJzIHdobyByZXZpZXdlZCBwcm9kdWN0cy4NCg0KDQojIyMjIEhvdyBtYW55IHNjb3JlcyBkbyB3ZSBoYXZlPw0KDQpgYGB7cn0NCmxlbmd0aChteV9kYXRhJGByZXZpZXcvc2NvcmVgKQ0KYGBgDQpUaGVyZSBhcmUgYHIgbGVuZ3RoKG15X2RhdGEkJ3Jldmlldy9zY29yZScpYCByYXRpbmdzLg0KDQojIyMjIFdoYXQgaXMgdGhlIGRpc3RyaWJ1dGlvbiBvZiByYXRpbmdzPw0KDQpgYGB7cn0NCmhpc3QoYXMubnVtZXJpYyhteV9kYXRhJGByZXZpZXcvc2NvcmVgKSxtYWluID0gIkhpc3RvZ3JhbW0gb2Ygc2NvcmVzIix4bGFiID0gIlNjb3JlIikNCmBgYA0KDQoNClByb2R1Y3RzIHNlZW0gdG8gYmUgZmF2b3JhYmx5IHJhdGVkIGFzIHRoZSBkaXN0cmlidXRpb24gb2Ygc2NvcmVzIHNob3dlcyB0aGF0IHRoZSBiZXN0IHNjb3JlIGlzIHRoZSBtb3N0IGZyZXF1ZW50Lg0KDQojIyMjIFdoYXQgaXMgdGhlIGF2ZXJhZ2UgbnVtYmVyIG9mIHJldmlld3MgcGVyIHVzZXI/DQoNCmBgYHtyLG1lc3NhZ2U9RkFMU0Usd2FybmluZz1GQUxTRX0NCm15X2RhdGEgJT4lIA0KICBncm91cF9ieShgcmV2aWV3L3VzZXJJZGApICU+JQ0KICBzdW1tYXJpc2UoRnJlcT1uKCkpICU+JSANCiAgc3VtbWFyeSgpDQpgYGANCkluIHRoZSBvcmlnaW5hbCBkYXRhIHNldCBJdCB1c2VycyBsZWZ0IG9uIGF2ZXJhZ2UgbGVmdCBhIHJldmlldyBvbmx5IG9uY2UuIEFmdGVyIGZpbHRlcmluZywgd2Ugc2VlIHRoYXQgb3VyIGF2ZXJhZ2UgaXMgYXQgMyByZXZpZXdzIHBlciB1c2VyLiANCg0KIyMjIyBXaGF0IGlzIHRoZSBhdmVyYWdlIHNjb3JlIHBlciB1c2VyPw0KDQpgYGB7cixtZXNzYWdlPUZBTFNFLHdhcm5pbmc9RkFMU0V9DQooZ3JhbmQubWVhbiA8LW15X2RhdGEgJT4lIA0KICBncm91cF9ieShgcmV2aWV3L3VzZXJJZGApICU+JQ0KICBkcGx5cjo6c3VtbWFyaXNlKE1lYW49bWVhbihgcmV2aWV3L3Njb3JlYCkpICU+JQ0KICBtdXRhdGUoR3JhbmQubWVhbj1tZWFuKE1lYW4pKSU+JQ0KICBoZWFkKCkpDQpgYGANCkl0IHNlZW1zIHRoYXQgYmVhdXR5IHByb2R1Y3RzIG9uIEFtYXpvbiBhcmUgd2VsbCByZWNlaXZlZCBieSB1c2VycyBhcyB0aGUgYXZlcmFnZSBzY29yZSBwZXIgdXNlciBpcyBxdWl0ZSBoaWdoLCBhdCBgciBhcy5udW1lcmljKGdyYW5kLm1lYW5bMSwzXSlgLg0KDQojIyBCdWlsZGluZyBhIG1vZGVsDQoNCiMjIyBGaW5hbCBkYXRhIG91dGxvb2sNCg0KSGVyZSBpcyBhIGdsaW1wc2UgaW4gb3VyIGRhdGEgYmVmb3JlIHdlIHN0YXJ0IGJ1aWxkaW5nIHRoZSByZWNvbW1uZGVyOg0KDQpgYGB7cn0NCihteV9kYXRhKQ0KYGBgDQoNCiMjIyBTdWJzZXR0aW5nIGRhdGENCg0KSW4gb3JkZXIgdG8gbW9kZWwgYSByZWNvbW1lbmRlciBzeXN0ZW0sIHRocmVlIHZhcmlhYmxlcyBpbiBvdXIgY2FzZSBhcmUgb2YgZ3JlYXQgaW1wb3J0YW5jZToNCg0KKiBVc2VyIElEDQoqIFByb2R1Y3QgSUQNCiogU2NvcmUgLyBSYXRpbmcNCg0KT3VyIG1vZGVsIHdpbGwgYmUgYmFzZWQgb24gdGhlc2UgdGhyZWUgdmFyaWFibGVzLiBBZGRpdGlvbmFsbHksIHdlIHdpbGwgbWFrZSB1c2Ugb2YgdGhlIHJlbWFpbmluZyBmZWF0dXJlcyBieSB1dGlsaXppbmcgc29tZSB0ZXh0IG1pbmluZyB0ZWNobmlxdWVzLCBidXQgeW91IHdpbGwgZmluZCBtb3JlIGRldGFpbHMgYXQgc29tZSBsYXRlciBwb2ludC4NCk5vdywgd2Ugd2lsbCBtYWtlIGEgc3Vic2V0IG9mIG91ciBkYXRhIHdpdGggMyBtZW50aW9uZWQgdmFyaWFibGVzOg0KDQpgYGB7cn0NCnN1YnNldF9teV9kYXRhIDwtIHN1YnNldChteV9kYXRhLCBzZWxlY3QgPSBjKGByZXZpZXcvdXNlcklkYCxgcHJvZHVjdC9wcm9kdWN0SWRgLGByZXZpZXcvc2NvcmVgKSkNCmhlYWQoc3Vic2V0X215X2RhdGEpDQoNCmBgYA0KDQpMZXQgdXMgaW5zcGVjdCB0aGUgZGltZW5zaW9uczoNCg0KYGBge3J9DQpkaW0oc3Vic2V0X215X2RhdGEpDQpgYGANCg0KIyMjIEZvcm1hdHRpbmcgZGF0YQ0KDQpPdXIgZGF0YSBpcyBjdXJyZW50bHkgaW4gdGhlIGxvbmcgZm9ybWF0LCBpLmUuIG9uZSByb3cgZm9yIG9uZSByYXRpbmcuIEhvd2V2ZXIsIHdlIHdvdWxkIHdhbnQgdG8gZ2V0IGEgbWF0cml4IHdpdGggcmF0aW5ncyB3aGVyZSB0aGUgcm93cyByZXByZXNlbnQgdGhlIHVzZXJzIElEcyBhbmQgdGhlIGNvbHVtbnMgdGhlIFByb2R1Y3QgSURzLg0KVGh1cywgd2Ugd2lsbCB0cmFuc2Zvcm0gb3VyIGRhdGEgdG8gc28gY2FsbGVkIHJhdGluZyBtYXRyaXg6DQoNCmBgYHtyfQ0KcmF0aW5ncyA8LSBhcyhzdWJzZXRfbXlfZGF0YSwgInJlYWxSYXRpbmdNYXRyaXgiKQ0KYGBgDQoNCkluIG9yZGVyIHRvIGF2b2lkICJoaWdoL2xvdyByYXRpbmcgYmlhcyIgZnJvbSB1c2VycyB3aG8gZ2l2ZSBoaWdoIChvciBsb3cpIHJhdGluZ3MgdG8gYWxsIHRoZSBwcm9kdWN0cyB0aGV5IHJldmlld2VkLCB3ZSB3aWxsIG5lZWQgdG8gbm9ybWFsaXplIG91ciBkYXRhLiBUaGF0IHdvdWxkIHByZXZlbnQgY2VydGFpbiBiaWFzIGluIHRoZSByZXN1bHRzLg0KDQpgYGB7cn0NCnJhdGluZ3MgPC0gbm9ybWFsaXplKHJhdGluZ3MpDQpgYGANCg0KIyMjIEluc3BlY3RpbmcgcmVhbCByYXRpbmcgbWF0cml4DQoNCldlIGNhbiBwbG90IGFuIGltYWdlIG9mIHRoZSByYXRpbmcgbWF0cml4IGZvciB0aGUgZmlyc3QgMjUwIHVzZXJzIGFuZCAyNTAgcHJvZHVjdHM6DQpgYGB7cn0NCmltYWdlKHJhdGluZ3NbMToyNTAsMToyNTBdKQ0KYGBgDQoNCkZyb20gdGhlIHZpc3VhbGlzYXRpb24gd2UgY2FuIHNlZSB0aGF0IHJhdGluZyBtYXRyaXggaXMgdmVyeSBzcGFyc2UsIGkuZS4gdGhhdCBub3QgZXZlcnkgdXNlciBkaWQgcmF0ZS9yZXZpZXcgZXZlcnkgcHJvZHVjdCBpbiBvdXIgZGF0YSBzZXQuIA0KDQpXZSBjYW4gaW5zcGVjdCB0aGUgZGF0YSBmb3IgdGhlIGZpcnN0IDEwIHVzZXJzIGFuZCB0aGUgZmlyc3QgNCBwcm9kdWN0czoNCg0KYGBge3J9DQpyYXRpbmdzWzE6MTAsIDE6NF1AZGF0YQ0KYGBgDQoNCkFzIHdlIGFscmVhZHkgc2F3IGluIHRoZSB2aXN1YWxpc2F0aW9uLCB0aGUgZGF0YSBpcyBzcGFyc2UgYW5kIHRoZSBmaXJzdCAxMCB1c2VycyBkaWQgbm90IHJldmlldyBmaXJzdCA0IHByb2R1Y3RzIHZpc3VhbGlzZWQgaW4gdGhlIG1hdHJpeCBhYm92ZS4NCg0KIyMjIEJ1aWxkaW5nIGEgcmVjb21tZW5kZXINCg0KRmluYWxseSwgd2Ugd2lsbCBub3cgYnVpbGQgb3VyIHJlY29tbWVuZGF0aW9uIHN5c3RlbSBiYXNlZCBvbiAqKlVzZXItYmFzZWQgY29sbGFib3JhdGl2ZSBmaWx0ZXJpbmcqKg0KVXNlci1iYXNlZCBjb2xsYWJvcmF0aXZlIGZpbHRlcmluZyBzZWFyY2ggZm9yIHNpbWlsYXIgdXNlcnMgYW5kIGdpdmVzIHRoZW0gcmVjb21tZW5kYXRpb25zIGJhc2VkIG9uIHdoYXQgb3RoZXIgdXNlcnMgd2l0aCBzaW1pbGFyIHJhdGluZyBwYXR0ZXJucyBhcHByZWNpYXRlZDoNCg0KYGBge3Isd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRX0NCnJlY29tbWVuZGVyIDwtIFJlY29tbWVuZGVyKHJhdGluZ3MsIG1ldGhvZD0iVUJDRiIpDQpyZWNvbW1lbmRlcg0KYGBgDQoNCkFkZGl0aW9uYWxseSwgaW4gb3JkZXIgdG8gY29tcGFyZSByZXN1bHRzIG9mIHR3byBtZXRob2RzLCAgd2Ugd291bGQgbGlrZSB0byBhcHBseSAqKml0ZW0tYmFzZWQgY29sbGFib3JhdGl2ZSBmaWx0ZXJpbmcqKiBtZXRob2QgdG8gYnVpbGQgYW5vdGhlciByZWNvbW1lbmRlciBzeXN0ZW0uIEluIGNvbnRyYXN0IHRvIHVzZXItYmFzZWQgY29sbGFib3JhdGl2ZSBmaWx0ZXJpbmcsIGl0ZW0tYmFzZWQgY29sbGFib3JhdGl2ZSBmaWx0ZXJpbmcgbG9va3MgZm9yIHNpbWlsYXJpdHkgcGF0dGVybnMgYmV0d2VlbiAqKml0ZW1zKiogYW5kIHJlY29tbWVuZHMgdGhlbSB0byB1c2VycyBiYXNlZCBvbiB0aGUgY29tcHV0ZWQgaW5mb3JtYXRpb24uDQoNCmBgYHtyfQ0KcmVjb21tZW5kZXJJQkNGIDwtIFJlY29tbWVuZGVyKHJhdGluZ3MsIG1ldGhvZD0iSUJDRiIpDQpyZWNvbW1lbmRlcklCQ0YNCmBgYA0KQXMgcmVwb3J0ZWQsIGJvdGggcmVjb21tZW5kYXRpb24gc3lzdGVtcyBhcmUgYnVpbHQgdXNpbmcgODAwMiB1c2Vycy4NCg0KDQojIyAgSW50ZXJwcmV0YXRpb24gYW5kIG1hbmFnZXJpYWwgaW1wbGljYXRpb25zDQoNCk5vdyB3ZSB3b3VsZCBsaWtlIHRvIGludGVycHJldCB0aGUgb3V0cHV0IG9mIG91ciByZWNvbW1lbmRlciBzeXN0ZW1zLiANCkZpcnN0IHdlIHN0YXJ0IHdpdGggVUJDRi1iYXNlZCByZWNvbW1lbmRlciBzeXN0ZW0uDQoNCmBgYHtyfQ0KY3VycmVudC51c2VyIDwtIDQ1DQpyZWNvbW1lbmRhdGlvbnMgPC0gcHJlZGljdChyZWNvbW1lbmRlciwgY3VycmVudC51c2VyLCBkYXRhID0gcmF0aW5ncywgbiA9IDUpDQpgYGANCg0KV2UgZGVjaWRlZCB0byB0YWtlIHVzZXIgbnVtYmVyIGByIGN1cnJlbnQudXNlcmAgYW5kIGluc3BlY3QgNSByZWNvbW1lbmRhdGlvbnMgcHJvdmlkZWQgdG8gaGltL2hlci4NCk5vdyB3ZSBjYW4gaW5zcGVjdCB3aGF0IG91ciByZWNvbW1lbmRhdGlvbiBzeXN0ZW0gcHJvdmlkZWQgaW4gdGhlIGVuZDoNCg0KYGBge3J9DQpzdHIocmVjb21tZW5kYXRpb25zKQ0KYGBgDQoNCldlIGNhbiBzZWUgdGhhdCB0aGUgdXNlciBJRCBvZiB0aGUgdXNlciBudW1iZXIgYHIgY3VycmVudC51c2VyYCBpcyBBMTBOMTlPTDBDS1lEVi4NCk91ciBzeXN0ZW0gZm91bmQgMiBwcm9kdWN0cyB0byByZWNvbW1lbmQgdG8gdGhpcyB1c2VyLCBhbmQgd2UgY2FuIGZpbmQgcHJvZHVjdCBpbmRleCAoMTczLCA3NzIpIGFzIHdlbGwgYXMgcmF0aW5ncyB0aGF0IHRoZSBzeXN0ZW0gY2FsY3VsYXRlZCBmcm9tIHRoZSByYXRpbmdzIG9mIHRoZSBjbG9zZXN0IHVzZXJzICg1LDUpLg0KDQpMZXQgdXMgY3JlYXRlIGEgcHJlZGljdGlvbiBtYWRlIGJ5IElCQ0YtYmFzZWQgcmVjb21tZW5kZXI6DQoNCmBgYHtyfQ0KcmVjb21tZW5kYXRpb25zSUJDRiA8LSBwcmVkaWN0KHJlY29tbWVuZGVySUJDRixjdXJyZW50LnVzZXIsZGF0YSA9IHJhdGluZ3MsIG49NSkNCnN0cihyZWNvbW1lbmRhdGlvbnNJQkNGKQ0KYGBgDQoNCldlIHdpbGwgaW5zcGVjdCBwb3RlbnRpYWwgcmVjb21tZW5kZWQgcHJvZHVjdHM6DQoNCmBgYHtyfQ0KaGVhZChhcyhyZWNvbW1lbmRhdGlvbnNJQkNGLCJsaXN0IikpDQpgYGANCg0KVW5mb3J0dW5hdGVseSwgb3VyIGl0ZW0tYmFzZWQgY29sbGFib3JhdGl2ZSBmaWx0ZXJpbmcgc3lzdGVtIGRpZCBub3QgZ2VuZXJhdGUgYW55IHJlY29tbWVuZGF0aW9uIGZvciB0aGUgdXNlciBudW1iZXIgYHIgY3VycmVudC51c2VyYC4NCg0KDQojIyMgSWRlbnRpZmljYXRpb24gb2YgdGhlIHJlY29tbWVuZGVkIHByb2R1Y3RzDQoNCkxldCB1cyBub3cgaWRlbnRpZnkgdGhlIHByb2R1Y3RzIHJlY29tbWVuZGVkIGJ5IFVCQ0YtYmFzZWQgcmVjb21tZW5kZXIuIEZpcnN0IHdlIG5lZWQgdG8gZXh0cmFjdCB0aGUgaW5kZXggb2YgdGhlIHJlY29tbWVuZGVkIHByb2R1Y3RzOiAgDQoNCmBgYHtyfQ0KaW5kZXg8LSBhcy52ZWN0b3IoYXMuZmFjdG9yKHVubGlzdChhcyhyZWNvbW1lbmRhdGlvbnMsICJsaXN0IikpKSkNCmBgYA0KDQpUaGVuIHdlIGZpbmQgY29ycmVzcG9uZGluZyBwcm9kdWN0IGluIG91ciBpbml0aWFsIGRhdGEgc2V0Og0KDQpgYGB7cn0NCihyZWNvbW1lbmRhdGlvbl8yNjwtbXlfZGF0YVttYXRjaChpbmRleCwgbXlfZGF0YSRgcHJvZHVjdC9wcm9kdWN0SWRgKSxdKSAgDQpgYGANCg0KVHdvIHByb2R1Y3RzIHJlY29tbWVuZGVkIGFyZSA6DQoNCiogYHIgcmVjb21tZW5kYXRpb25fMjYkJ3Byb2R1Y3QvdGl0bGUnWzFdYCAtICBmYWNpYWwgY2xlYW5zaW5nIGNyZWFtDQoqIGByIHJlY29tbWVuZGF0aW9uXzI2JCdwcm9kdWN0L3RpdGxlJ1syXWAgLSAgY29sb3IgZm9yIGhhaXINCg0KDQpMZXQgdXMgbm93IGluc3BlY3QgcHJvZHVjdHMgdGhhdCB0aGUgdXNlciBBMTBOMTlPTDBDS1lEViByYXRlZDoNCg0KYGBge3J9DQpteV9kYXRhW21hdGNoKCJBMTBOMTlPTDBDS1lEViIsbXlfZGF0YSRgcmV2aWV3L3VzZXJJZGApLF0NCmBgYA0KDQojIyMgSW1wbGljYXRpb25zDQoNCkFzIHdlIGNvdWxkIHNlZSwgdGhpcyB1c2VyIHJldmlld2VkIG9ubHkgb25lIHByb2R1Y3QsIGNhbGxlZCAiT3BpIFJpZGdlIEZpbGxlciAuNSBvei4iLCBhbmQgaXQgaXMgYSBuYWlsLWNhcmUgcHJvZHVjdC4gV2UgY291bGQgYXNzdW1lIHRoYXQgdGhpcyBwZXJzb24gaXMgYSBmZW1hbGUgdXNlciBzaW5jZSB0aGUgcHJvZHVjdCBzaGUgYm91Z2h0IGlzIHR5cGljYWxseSBhc3NvY2lhdGVkIHdpdGggZmVtYWxlIGJlYXV0eSBjYXJlLiBXaGF0IGlzIG1vcmUsIHR3byByZWNvbW1lbmRlZCBwcm9kdWN0cyBhcmUgYXMgd2VsbCB2ZXJ5IHN0cm9uZ2x5IGFzc29jaWF0ZWQgdG8gYmVpbmcgdHlwaWNhbCBmZW1hbGUgYmVhdXR5IHByb2R1Y3RzLiBGaW5hbGx5LCB3ZSBoYXZlIHRoZSBuYW1lIG9mIHRoZSB1c2VyIChFcmljYSksIHNvIHdlIGNhbiBiZSBzdXJlIHRoYXQgdGhlIHVzZXIgaXMgYSBmZW1hbGUuDQpGcm9tIHRoZSBxdWFsaXRhdGl2ZSBwZXJzcGVjdGl2ZSBpdCBzZWVtcyB0aGF0IG91ciByZWNvbW1lbmRhdGlvbiBzeXN0ZW0gcHJvdmlkZXMgZGVzY2VudCByZWNvbW1lbmRhdGlvbnMhLg0KDQoNCiMjIEJvbnVzIGFuYWx5c2lzOiBUZXh0IE1pbmluZw0KDQpJbiBhZGRpdGlvbiB0byBvdXIgcmVjb21tZW5kZXIgc3lzdGVtLCB3ZSB3aWxsIGFwcGx5IHNvbWUgYmFzaWMgdGV4dCBtaW5pbmcgdGVjaG5pcXVlcyB0byBleHBsb3JlIHJldmlld3MgdGV4dC4gVGV4dCBtaW5pbmcgaGVscHMgdXMgdG8gbWluZSBvcGluaW9ucyBvZiB1c2VycyAoaW4gdGhpcyBjYXNlKSBhYm91dCB0aGUgcmV2aWV3ZWQgcHJvZHVjdHMgYXQgc2NhbGUuDQoNCiMjIyBXb3JkY2xvdWQgDQoNCkhlcmUgd2UgY3JlYXRlIGEgd29yZGNsb3VkIG9mIHdvcmRzIGZyb20gcHJvZHVjdCByZXZpZXdzIG9mIHJlY29tbWVuZGVkIHByb2R1Y3RzIHRvIHRoZSB1c2VyIGByIGN1cnJlbnQudXNlcmAuIEJlZm9yZWhhbmQgd2Ugd291bGQgbmVlZCB0byBwcmUtcHJvY2VzcyB0aGUgdGV4dCBvZiByZXZpZXdzIGluIHRoZSBmb2xsb3dpbmcgbWFubmVyOiANCg0KYGBge3Isd2FybmluZz1GQUxTRSxtZXNzYWdlPUZBTFNFfQ0KIyBTcGxpdCB0ZXh0IGludG8gcGFydHMgdXNpbmcgbmV3IGxpbmUgY2hhcmFjdGVyOg0KdGV4dC5kb2NzIDwtIENvcnB1cyhWZWN0b3JTb3VyY2UocmVjb21tZW5kYXRpb25fMjYkYHJldmlldy90ZXh0YCkpDQp0b1NwYWNlIDwtIGNvbnRlbnRfdHJhbnNmb3JtZXIoZnVuY3Rpb24gKHggLCBwYXR0ZXJuICkgZ3N1YihwYXR0ZXJuLCAiICIsIHgpKQ0KdGV4dC5kb2NzIDwtIHRtX21hcCh0ZXh0LmRvY3MsIHRvU3BhY2UsICIvIikNCnRleHQuZG9jcyA8LSB0bV9tYXAodGV4dC5kb2NzLCB0b1NwYWNlLCAiQCIpDQp0ZXh0LmRvY3MgPC0gdG1fbWFwKHRleHQuZG9jcywgdG9TcGFjZSwgIlxcfCIpDQp0ZXh0LmRvY3MgPC0gdG1fbWFwKHRleHQuZG9jcywgY29udGVudF90cmFuc2Zvcm1lcih0b2xvd2VyKSkNCnRleHQuZG9jcyA8LSB0bV9tYXAodGV4dC5kb2NzLCByZW1vdmVOdW1iZXJzKQ0KdGV4dC5kb2NzIDwtIHRtX21hcCh0ZXh0LmRvY3MsIHN0cmlwV2hpdGVzcGFjZSkNCnRleHQuZG9jcyA8LSB0bV9tYXAodGV4dC5kb2NzLCByZW1vdmVXb3Jkcywgc3RvcHdvcmRzKCJlbmdsaXNoIikpDQp0ZXh0LmRvY3MgPC0gdG1fbWFwKHRleHQuZG9jcywgcmVtb3ZlUHVuY3R1YXRpb24pDQpkdG0gPC0gRG9jdW1lbnRUZXJtTWF0cml4KHRleHQuZG9jcywgY29udHJvbD1saXN0KHdlaWdodGluZz13ZWlnaHRUZikpDQptIDwtIGFzLm1hdHJpeCh0KGR0bSkpDQp2IDwtIHNvcnQocm93U3VtcyhtKSxkZWNyZWFzaW5nPVRSVUUpDQpkIDwtIGRhdGEuZnJhbWUod29yZCA9IG5hbWVzKHYpLGZyZXE9dikNCnNldC5zZWVkKDEyMzQpDQp3b3JkY2xvdWQod29yZHMgPSBkJHdvcmQsIGZyZXEgPSBkJGZyZXEsIG1pbi5mcmVxID0gMTAsDQogICAgICAgICAgbWF4LndvcmRzPTIwMCwgcmFuZG9tLm9yZGVyPUZBTFNFLCByb3QucGVyPTAuMzUsDQogICAgICAgICAgY29sb3JzPWJyZXdlci5wYWwoOCwgIkRhcmsyIikpDQpgYGANCg0KRnJvbSB0aGUgd29yZGNsb3VkIHdlIGNhbiBzZWUgdGhhdCB3b3JkcyAiY29sb3IiLCAiaGFpciIgYW5kICJnbG92ZXMiIGFyZSBxdWl0ZSBmcmVxdWVudCBpbiB0aGUgdGV4dCBjb3JwdXMgYW5hbHl6ZWQuIFRoYXQgY291bGQgYmUgYSBoaW50IHRoYXQgdGhlIHVzZXIgd2FzIHJlZmVycmluZyB0byB0aGUgdXNhZ2Ugb2YgdGhlIHByb2R1Y3QuDQpUaGUgdGVybSAiY2hlYXAiIGNvdWxkIGJlIGVhc2lseSBzcG90dGVkIGFzIHdlbGwuIFRoaXMgd29yZCBpcyBub3QgdmVyeSBsaWthYmxlIGFtb25nIG1hcmtldGVycyBhcyBpdCBicmluZ3MgdW5mYXZvcmFibGUgaW1hZ2UgdG8gdGhlIGJyYW5kLiBOZXZlcnRoZWxlc3MsIGl0IHNlZW1zIHRoYXQgdGhlIHVzZXIgYmVsaWV2ZXMgdGhhdCB0aGUgcHJvZHVjdCBpcyBhZmZvcmRhYmxlLg0KDQoNCiMjIEZ1dHVyZSB3b3JrDQoNClRoaXMgZGF0YSBzZXQgcHJvdmlkZXMgbXVsdGlwbGUgcG9zc2liaWxpdHkgZm9yIHRoZSBmdXJ0aGVyIGFuYWx5c2lzIGJlc2lkZXMgcmVjb21tZW5kZXIgc3lzdGVtcy4NCkhlcmUgYXJlIHNvbWUgaWRlYXMgd2hhdCBjYW4gYmUgZnVydGhlciBkb25lOg0KDQoqICoqU2VudGltZW50IGFuYWx5c2lzKiogLSBTZW50aW1lbnQgYW5hbHlzaXMgY2FuIGJlIGRvbmUgYW5kIHNjb3JlcyAodHlwaWNhbGx5IGZyb20gLTMgdG8gKzMpIGFjY29tcGFuaWVkIHRvIGVhY2ggcmV2aWV3IGRlc2NyaXB0aW9uLiBUaGF0IHdvdWxkIHRlbGwgdXMgbW9yZSBhYm91dCB0aGUgc2VudGltZW50IHRoYXQgdXNlcnMgaGF2ZSBhYm91dCB0aGUgcHJvZHVjdHMgcmV2aWV3ZWQuDQoNCiogKipQcmVkaWN0aW9uIG9mIHJhdGluZ3MqKiAtIEluIGNhc2UgdGhhdCB3ZSB3b3VsZCBoYXZlIGVub3VnaCBkYXRhIChyYXRpbmdzKSBhYm91dCBvbmUgcHJvZHVjdCwgcmVnYXJkbGVzcyBvZiBjdXN0b21lcnMsIGl0IHdvdWxkIGJlIHBvc3NpYmxlIHRvIGRldmVsb3AgYSBtYWNoaW5lIGxlYXJuaW5nIG1vZGVsIHdoaWNoIGJhc2VkIG9uIGN1cnJlbnQgZmVhdHVyZXMgKGUuZy4gcHJpY2UpIGFuZCBhZGRpdGlvbmFsIGZlYXR1cmVzIChzdWNoIGFzIHNlbnRpbWVudCBvciB3b3JkcyBpbiB0aGUgcmV2aWV3KSBjb3VsZCBwcmVkaWN0IHRoZSByYXRpbmcgdGhhdCBvbmUgcHJvZHVjdCBtaWdodCBoYXZlLg0KDQoqICoqUHJlZGljdGlvbiBvZiB0aGUgc2VudGltZW50KiogLSBpbiB0aGUgc2ltaWxhciBtYW5uZXIgYXMgdGhlIHByZXZpb3VzIHBvaW50LCBpdCB3b3VsZCBiZSB1c2VmdWwgdG8gdHJhaW4gYSBtYWNoaW5lIGxlYXJuaW5nIG1vZGVsIHRvIHByZWRpY3QgYSBzZW50aW1lbnQgdGhhdCB3b3VsZCBoeXBvdGV0aWNhbGx5IGVtZXJnZSBpbiBhIHJldmlld2VyLg0KDQoqICoqVG9waWMgbW9kZWxpbmcqKiAtIHRvcGljIG1vZGVsaW5nIGlzIHVuc3VwZXJ2aXNlZCBtYWNoaW5lIGxlYXJuaW5nIHRlY2huaXF1ZSB0aGF0IGNvdWxkIGhlbHAgdXMgaWRlbnRpZnkgdG9waWNzIHdoaWNoIHVzZXJzIGRpc2N1c3MgaW4gdGhlIHRleHQgb2YgcmV2aWV3cy4gDQoNCiMjIExpbWl0YXRpb25zDQoNCkxpbWl0YXRpb24gcmVsYXRlZCB0byB0aGlzIGRhdGEgc2V0IGFuZCBidWlsZGluZyBhIHJlY29tbWVuZGVyIHN5c3RlbSBpcyB0aGUgZmFjdCB0aGF0IHRoZSBtYWpvcml0eSBvZiB1c2VycyBoYXZlIGxlZnQgb25seSBvbmUgcmV2aWV3Og0KDQpgYGB7cn0NCnRhYmxlKGFzLmRhdGEuZnJhbWUodGFibGUobXlfZGF0YSRgcmV2aWV3L3VzZXJJZGApKSRGcmVxKQ0KYGBgDQoNCkxldCB1cyB0YWtlIGEgbG9vayB3aGljaCB1c2VycyBsZWZ0IHRoZSBtb3N0IHJldmlld3M6DQoNCmBgYHtyfQ0KbGltaXRhdGlvbnMgPC1hcy5kYXRhLmZyYW1lKHRhYmxlKG15X2RhdGEkYHJldmlldy91c2VySWRgKSkNCmxpbWl0YXRpb25zICU+JSBhcnJhbmdlKGRlc2MoRnJlcSkpJT4lcmVuYW1lKFVzZXJJRD1WYXIxKQ0KYGBgDQoNCldlIGNhbiBzZWUgdGhhdCB1c2VycyB1bmRlciBJRHMgQTNNMTc0SUMwVlhPUzIsQTNLRVpMSjU5QzFKVkgsQTNRRUUwWlBNVDNXNlAgYXJlIHJhcmUgZXhhbXBsZXMgb2YgdXNlcnMgd2hvIGxlZnQgbXVsdGlwbGUgcHJvZHVjdCByZXZpZXdzLg0K